package org.dftproject.genesis.ui.pages.pedigree;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dftproject.genesis.data.IEntity;
import org.dftproject.genesis.data.genealogy.GenealogyUtils;
import org.dftproject.genesis.data.genealogy.IPerson;
import org.dftproject.genesis.data.genealogy.impl.MergedPerson;
import org.dftproject.genesis.ui.Activator;
import org.dftproject.genesis.ui.figures.dualtree.DualTreeFigure;
import org.dftproject.genesis.ui.figures.dualtree.ISelectableFigure;
import org.dftproject.genesis.ui.figures.dualtree.ImprovedWalkersLayout;
import org.dftproject.genesis.ui.figures.zoompan.ZoomPanContainer;
import org.dftproject.genesis.ui.pages.AbstractPagePart;
import org.dftproject.genesis.ui.pages.IPageInput;
import org.dftproject.genesis.ui.pages.IPageSite;
import org.dftproject.genesis.ui.pages.PageInitException;
import org.dftproject.genesis.ui.pages.PageManager;
import org.dftproject.genesis.ui.pages.circlediagram.CircleDiagramIconFigure;
import org.dftproject.genesis.ui.pages.circlediagram.CircleDiagramPage;
import org.dftproject.genesis.ui.pages.circlediagram.CircleDiagramPageInput;
import org.dftproject.genesis.ui.pages.pedigree.icon.PedigreeIconFigure;
import org.dftproject.genesis.ui.pages.pedigree.tree.CoupleFigure;
import org.dftproject.genesis.ui.pages.pedigree.tree.PedigreeAnimator;
import org.dftproject.genesis.ui.pages.pedigree.zoom.ZoomControlFigure;
import org.dftproject.genesis.ui.pages.pedigree.zoom.ZoomManager;
import org.dftproject.genesis.ui.pages.summary.SummaryPage;
import org.dftproject.genesis.ui.pages.summary.SummaryPageInput;
import org.dftproject.genesis.ui.widgets.gallery.Gallery;
import org.eclipse.draw2d.Animation;
import org.eclipse.draw2d.DelegatingLayout;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.LayoutListener;
import org.eclipse.draw2d.LightweightSystem;
import org.eclipse.draw2d.Locator;
import org.eclipse.draw2d.MouseListener;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;

public class PedigreePage extends AbstractPagePart {

	private static final Log log = LogFactory.getLog(PedigreePage.class);

	public static final String ID = PedigreePage.class.getName();

	private Canvas canvas;
	private ZoomPanContainer zoomPanContainer;
	private DualTreeFigure pedigree;

	private static final int numThreads = 5;
	private ThreadPoolExecutor threadPool;

	private final Set<IEntity> alreadyVisited = Collections.synchronizedSet(new HashSet<IEntity>());

	private boolean updateScheduled;
	private final Queue<Couple> updateQueue = new LinkedList<Couple>();
	private final Runnable uiRunnable = new Runnable() {

		public void run() {
			updateScheduled = false;
			update();
		}

	};

	private MergedPerson person;

	public void init(IPageSite site, IPageInput input) throws PageInitException {
		if (!(input instanceof PedigreePageInput))
			throw new PageInitException("input must be a " + PedigreePageInput.class.getName());
		setInput(input);

		threadPool = new ThreadPoolExecutor(numThreads, numThreads, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
		
		person = getMergedPerson(getPedigreePageInput().getPerson());
	}

	public PedigreePageInput getPedigreePageInput() {
		return (PedigreePageInput) this.getInput();
	}

	protected MergedPerson getMergedPerson(IPerson person) {
		if (person instanceof MergedPerson || person == null)
			return (MergedPerson) person;
		MergedPerson mergePerson = new MergedPerson(person);
		return mergePerson;
	}

	@Override
	public void createPartControl(Composite parent) {
		Composite container = new Composite(parent, SWT.NONE);
		GridLayout layout = new GridLayout();
		layout.marginWidth = layout.marginHeight = layout.marginTop = layout.marginRight = layout.marginBottom = layout.marginLeft = layout.horizontalSpacing = 0;
		layout.numColumns = 2;
		container.setLayout(layout);

		createGallery(container);

		Composite body = new Composite(container, SWT.NONE);
		layout = new GridLayout();
		layout.marginWidth = layout.marginHeight = layout.marginTop = layout.marginRight = layout.marginBottom = layout.marginLeft = layout.verticalSpacing = 0;
		body.setLayout(layout);
		body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

		createContents(body);
	}

	protected void createGallery(Composite parent) {
		Gallery gallery = new Gallery(parent);
		gallery.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, false, true));

		gallery.addItem(Activator.getImageDescriptor("icons/user.png").createImage(), false, "Summary", new Runnable() {

			public void run() {
				IWorkbench workbench = PlatformUI.getWorkbench();
				IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
				IWorkbenchPage workbenchPage = window.getActivePage();
				PageManager.getDefault().openPage(workbenchPage, new SummaryPageInput(person), SummaryPage.ID, true, false);
			}

		});
		gallery.addItem(new PedigreeIconFigure(), true, "Pedigree", null);
		gallery.addItem(new CircleDiagramIconFigure(), false, "Circle Diagram", new Runnable() {

			public void run() {
				IWorkbench workbench = PlatformUI.getWorkbench();
				IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
				IWorkbenchPage workbenchPage = window.getActivePage();
				PageManager.getDefault().openPage(workbenchPage, new CircleDiagramPageInput(person), CircleDiagramPage.ID, true, false);
			}

		});
	}

	protected void createContents(Composite parent) {
		// Create the main canvas and lightweight system

		canvas = new Canvas(parent, SWT.DOUBLE_BUFFERED | SWT.NO_BACKGROUND);
		canvas.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

		LightweightSystem lws = new LightweightSystem(canvas);
		final IFigure root = new Figure();
		lws.setContents(root);
		
		root.setLayoutManager(new DelegatingLayout());
		
		zoomPanContainer = new ZoomPanContainer();
		root.add(zoomPanContainer, new Locator() {

			public void relocate(IFigure target) {
				target.setBounds(root.getBounds());
			}
		
		});
		
		// Create the pedigree container
		
		pedigree = new DualTreeFigure();
		pedigree.setLayoutManager(new ImprovedWalkersLayout());
		pedigree.addLayoutListener(PedigreeAnimator.getDefault());
		pedigree.addLayoutListener(new LayoutListener.Stub() {

			@Override
			public void postLayout(IFigure container) {
				zoomPanContainer.maintainFocus();
			}

		});
		zoomPanContainer.getContents().add(pedigree);

		// Create zoom manager and control

		final ZoomManager zoomManager = new ZoomManager(zoomPanContainer);
		canvas.addMouseWheelListener(new MouseWheelListener() {

			public void mouseScrolled(MouseEvent e) {
				zoomManager.setExponent(zoomManager.getExponent() + (e.count > 0 ? 1 : -1), e.count > 0 ? new Point(e.x, e.y) : zoomPanContainer.getViewport().getBounds().getCenter());
			}

		});

		final IFigure zoomControl = new ZoomControlFigure(zoomManager);
		root.add(zoomControl, new Locator() {

			public void relocate(IFigure target) {
				Dimension size = target.getPreferredSize();
				target.setBounds(new Rectangle(10, 10, size.width, size.height));
			}
		
		});

		// Handle arrow keys

		canvas.addKeyListener(new KeyAdapter() {

			@Override
			public void keyPressed(KeyEvent e) {
				if (pedigree.getSelectedNode() == null)
					return;

				switch (e.keyCode) {
				case SWT.ARROW_LEFT:
					ISelectableFigure left = getLeft(pedigree.getSelectedNode());
					if (left != null) {
						left.setSelected(true);
						zoomPanContainer.smoothScrollTo(zoomPanContainer.getFigureLocation(left));
					}
					break;
				case SWT.ARROW_RIGHT:
					ISelectableFigure right = getRight(pedigree.getSelectedNode());
					if (right != null) {
						right.setSelected(true);
						zoomPanContainer.smoothScrollTo(zoomPanContainer.getFigureLocation(right));
					}
					break;
				case SWT.ARROW_UP:
					ISelectableFigure up = getUp(pedigree.getSelectedNode());
					if (up != null) {
						up.setSelected(true);
						zoomPanContainer.smoothScrollTo(zoomPanContainer.getFigureLocation(up));
					}
					break;
				case SWT.ARROW_DOWN:
					ISelectableFigure down = getDown(pedigree.getSelectedNode());
					if (down != null) {
						down.setSelected(true);
						zoomPanContainer.smoothScrollTo(zoomPanContainer.getFigureLocation(down));
					}
					break;
				case SWT.CR:
					if (pedigree.getSelectedNode() != null) {
						open((CoupleFigure) pedigree.getSelectedNode());
					}
					break;
				}
			}

		});

		// Start loading the pedigree

		loadPedigree();
	}

	protected void open(CoupleFigure figure) {
		IWorkbench workbench = PlatformUI.getWorkbench();
		IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
		IWorkbenchPage workbenchPage = window.getActivePage();
		PageManager.getDefault().openPage(workbenchPage, new PedigreePageInput(figure.getPerson1()), PedigreePage.ID, true, false);
	}

	protected ISelectableFigure getLeft(IFigure figure) {
		IFigure[] descendants = pedigree.getDescendants(figure);
		if (descendants.length == 0 || descendants[0] == null)
			return null;
		return (ISelectableFigure) descendants[0];
	}

	protected ISelectableFigure getRight(IFigure figure) {
		ISelectableFigure nearest = null;

		int y = figure.getBounds().getCenter().y;
		IFigure[] ancestors = pedigree.getAncestors(figure);
		for (IFigure ancestor : ancestors) {
			if (!(ancestor instanceof ISelectableFigure))
				continue;
			if (nearest == null)
				nearest = (ISelectableFigure) ancestor;
			else if (Math.abs(nearest.getBounds().getCenter().y - y) > Math.abs(ancestor.getBounds().getCenter().y - y))
				nearest = (ISelectableFigure) ancestor;
		}

		return nearest;
	}

	protected ISelectableFigure getUp(IFigure figure) {
		// Get this figure's child

		IFigure[] descendants = pedigree.getDescendants(figure);
		if (descendants.length == 0 || descendants[0] == null)
			return null;

		// Get the ancestor index of this figure

		int index;
		IFigure[] ancestors = pedigree.getAncestors(descendants[0]);
		for (index = 0; index < ancestors.length; index++) {
			if (ancestors[index] == figure)
				break;
		}

		// Return the ancestor with the next highest index

		if (index > 0) {
			for (int i = index - 1; index >= 0; index--) {
				if (ancestors[i] != null)
					return (ISelectableFigure) ancestors[i];
			}
		}

		// Try moving down a generation

		IFigure up = descendants[0];
		while ((up = getUp(up)) != null) {
			ancestors = pedigree.getAncestors(up);
			for (int i = ancestors.length - 1; i >= 0; i--) {
				if (ancestors[i] != null)
					return (ISelectableFigure) ancestors[i];
			}
		}

		return null;
	}

	protected ISelectableFigure getDown(IFigure figure) {
		// Get this figure's child

		IFigure[] descendants = pedigree.getDescendants(figure);
		if (descendants.length == 0 || descendants[0] == null)
			return null;

		// Get the ancestor index of this figure

		int index;
		IFigure[] ancestors = pedigree.getAncestors(descendants[0]);
		for (index = ancestors.length - 1; index >= 0; index--) {
			if (ancestors[index] == figure)
				break;
		}

		// Return the ancestor with the next highest index

		if (index < ancestors.length - 1) {
			for (int i = index + 1; index < ancestors.length; index++) {
				if (ancestors[i] != null)
					return (ISelectableFigure) ancestors[i];
			}
		}

		// Try moving down a generation

		IFigure down = descendants[0];
		while ((down = getDown(down)) != null) {
			ancestors = pedigree.getAncestors(down);
			for (int i = 0; i < ancestors.length; i++) {
				if (ancestors[i] != null)
					return (ISelectableFigure) ancestors[i];
			}
		}

		return null;
	}

	protected void loadPedigree() {
		threadPool.execute(new Runnable() {

			public void run() {
				MergedPerson personA = person;
				MergedPerson personB = null;

				Set<IPerson> spouses = GenealogyUtils.getSpouses(personA);
				if (spouses != null && !spouses.isEmpty())
					personB = getMergedPerson(spouses.iterator().next());
				if (personA != null || personB != null)
					addCouple(new Couple(personA, personB));
			}

		});
	}

	@Override
	public void setFocus() {
		canvas.setFocus();
	}

	protected void scheduleUIUpdate(Couple item) {
		synchronized (updateQueue) {
			updateQueue.add(item);
			if (!updateScheduled) {
				updateScheduled = true;
				Display.getDefault().asyncExec(uiRunnable);
			}
		}
	}

	protected void update() {
		synchronized (updateQueue) {
			Animation.markBegin();

			Couple couple;
			while ((couple = updateQueue.poll()) != null) {
				createCoupleFigure(couple);
			}

			Animation.run();
		}
	}

	protected void addCouple(final Couple couple) {
		scheduleUIUpdate(couple);

		threadPool.execute(new Runnable() {

			public void run() {
				MergedPerson person1 = couple.getPerson1();
				if (person1 != null) {
					boolean visited = false;
					for (IEntity entity : person1.getEntities())
						if (!alreadyVisited.add(entity))
							visited = true;
					if (!visited)
						addParents(couple, true);
					else
						log.info(person1.toString() + " already seen");
				}

				MergedPerson person2 = couple.getPerson2();
				if (person2 != null) {
					boolean visited = false;
					for (IEntity entity : person2.getEntities())
						if (!alreadyVisited.add(entity))
							visited = true;
					if (!visited)
						addParents(couple, false);
					else
						log.info(person2.toString() + " already seen");
				}
			}

		});
	}

	protected void addParents(final Couple couple, final boolean paternal) {
		if (couple == null)
			return;
		MergedPerson person = paternal ? couple.getPerson1() : couple.getPerson2();
		if (person == null)
			return;
		MergedPerson parentA = getMergedPerson(GenealogyUtils.getFather(person));
		MergedPerson parentB = getMergedPerson(GenealogyUtils.getMother(person));

		Couple parents = new Couple(parentA, parentB);
		parents.setChild(couple, paternal);

		addCouple(parents);
	}

	protected void createCoupleFigure(Couple couple) {
		if (couple.getPerson1() == null && couple.getPerson2() == null) {
			CoupleFigure childFigure = couple.getChild().getFigure();
			if (couple.isPaternal())
				childFigure.setLeft(false);
			else
				childFigure.setRight(false);
			return;
		}

		final CoupleFigure figure = new CoupleFigure(couple.getPerson1(), couple.getPerson2());
		figure.addMouseListener(new MouseListener.Stub() {

			@Override
			public void mouseDoubleClicked(org.eclipse.draw2d.MouseEvent me) {
				open(figure);
			}

		});
		figure.addPropertyChangeListener(ISelectableFigure.SELECTION_PROPERTY, new PropertyChangeListener() {

			public void propertyChange(PropertyChangeEvent event) {
				if (Boolean.TRUE.equals(event.getNewValue()))
					zoomPanContainer.setFocus(figure);
			}

		});

		if (couple.getChild() != null) {
			CoupleFigure childFigure = couple.getChild().getFigure();
			if (couple.isPaternal()) {
				childFigure.setLeft(true);
				pedigree.addAncestor(childFigure, figure, 0);
			} else {
				childFigure.setRight(true);
				pedigree.addAncestor(childFigure, figure, 1);
			}
		} else {
			pedigree.setRoot(figure);
			figure.setSelected(true);
		}

		couple.setFigure(figure);
	}

	protected class Couple {

		private MergedPerson person1;
		private MergedPerson person2;
		private Couple child;
		private boolean paternal;
		private CoupleFigure figure;

		public Couple(MergedPerson person1, MergedPerson person2) {
			this.person1 = person1;
			this.person2 = person2;
		}

		public MergedPerson getPerson1() {
			return person1;
		}

		public void setPerson1(MergedPerson person1) {
			this.person1 = person1;
		}

		public MergedPerson getPerson2() {
			return person2;
		}

		public void setPersonB(MergedPerson person2) {
			this.person2 = person2;
		}

		public Couple getChild() {
			return child;
		}

		public boolean isPaternal() {
			return paternal;
		}

		public void setChild(Couple child, boolean paternal) {
			this.child = child;
			this.paternal = paternal;
		}

		public CoupleFigure getFigure() {
			return figure;
		}

		public void setFigure(CoupleFigure figure) {
			this.figure = figure;
		}

	}

}
